# $Id: radius.py 7557b5d4d82b 2012/12/02 09:07:16 pgurumur $
# Copyright (c) 1999, Stuart Bishop <zen@shangri-la.dropbear.id.au> 
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
#     Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 
#     Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the
#     distribution.
# 
#     The name of Stuart Bishop may not be used to endorse or promote 
#     products derived from this software without specific prior written 
#     permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# RADIUS client implementation using python, uses udp module for socket
# related utilities

# essential imports
import hashlib
import random
import struct
import StringIO

import pyrad.packet
from pyrad.client import Client, Timeout
from pyrad.dictionary import Dictionary

# Local imports
from djanet import utils
from djanet.lib import stdlib, udp
from djanet.lib.exception import RadiusError

__version__ = "$Revision: 7557b5d4d82b $"
__author__ = "$Author: pgurumur $"

class Radius(object):
   def __init__(self, Host, Secret, Port = 1812, Retries = 3, Timeout = 5):
      super(Radius, self).__init__()
      self._log = utils.loghandle(stdlib.my_name(self))
      self._dictionary = u"""
      ATTRIBUTE User-Name  1 string
      ATTRIBUTE User-Password 2 string encrypt=1
      """

      try:
         self._client = Client(server = Host, authport = Port,
               secret = Secret.encode('utf-8'),
               dict = Dictionary(StringIO.StringIO(self._dictionary)),)
      except AttributeError, error:
         self._error(str(error))

   def _error(self, Message):
      if Message:
         raise RadiusError, Message

   def Authenticate(self, Username, Password):
      retval = None
      req = self._client.CreateAuthPacket(code = pyrad.packet.AccessRequest,
            User_Name = Username.encode('utf-8'), )
      req["User-Password"] = req.PwCrypt(Password.encode('utf8'))

      reply = None
      try:
         reply = self._client.SendPacket(req)
      except Timeout, error:
         self._error(str(error))
      except Exception, error:
         self._error(str(error))
      else:
         if reply.code == pyrad.packet.AccessReject:
            retval = False
         elif reply.code != pyrad.packet.AccessAccept:
            self._log("Unknown code: %s", reply.code)
         else:
            retval = True

      return retval

# radius class, inherits udp class
class radius(udp.udp):
   # constructor, take host, secret, everything else defaults,
   # these days RADIUS port is 1812, Retries and Timeout are standard
   def __init__ (self, Host, Secret, Port = 1812, Retries = 3, Timeout = 5):
      # Instantiate the UDP class
      super(radius, self).__init__(Host, Port)
      self._log = utils.loghandle(stdlib.my_name(self))

      # Instantite the RADIUS Exception class
      self._err = RadiusError()

      # Standard RADIUS attributes
      self._AccessRequest = 1
      self._AccessAccept = 2
      self._AccessReject = 3

      # few variables
      self._secret = None
      self._retries = 0
      self._timeout = 0

      if Secret:
         self._secret = Secret.encode('ascii')
      else:
         self._error("no secret defined!")

      if Retries:
         try:
            self._retries = int(Retries)
         except ValueError, message:
            self._error(message)
      else:
         self._retries = 3

      if Timeout:
         try:
            self._timeout = int(Timeout)
         except ValueError, message:
            self._error(message)
      else:
         self._timeout = 5

   def _error(self, Message):
      if Message:
         raise RadiusError, Message

   # Generate authenticator based on secret
   def crypt(self, Authenticator, Text):
      vector = hashlib.md5(self._secret + Authenticator).digest()
      secret = ""

      for ix in xrange(0, len(Text)):
         if ((ix % 16) == 0) and (ix != 0):
            vector = hashlib.md5(self._secret + secret[-16:]).digest()

         secret += chr(ord(vector[ix]) ^ ord(Text[ix])).encode('utf-8')

      for ix in xrange(len(secret), 16):
         secret += vector[ix]

      return secret.encode('utf-8')

   # Authenicate the user
   def Authenticate(self, Username, Password):
      retval = None
      rand = random.randint(0, 255)
      vector = range(0, 17)
      vector[0] = "16B"
      for jy in xrange(1, 17):
         vector[jy] = random.randint(1, 127)

      authenticator = apply(struct.pack, vector)
      passwd = None
      if Password:
         passwd = self.crypt(
               authenticator.encode('ascii'), Password.encode('ascii'))
      else:
         self._error("no password supplied for %s" %Username)

      if Username and passwd:
         mesg = struct.pack("!BBH16sBB%dsBB%ds" %(
            len(Username), len(passwd)), 1, rand, len(Username) + len(passwd) +
            24, authenticator, 1, len(Username) + 2, Username, 2,
            len(passwd) + 2, passwd)

         for ix in xrange(0, self._retries):
            response = None
            try:
               if self.Write(mesg, self._timeout):
                  (response, addr) = self.Read(4096, self._timeout)
               else:
                  continue
            except udp.SocketError, message:
               self._error(message)
            else:
               if ord(response[1]) != rand:
                  continue

               checkAuth = response[4:20]
               cksum = hashlib.md5(response[0:4] + authenticator +
                     response[20:] + self._secret).digest()

               if cksum != checkAuth:
                  continue

               if (ord(response[0]) == self._AccessAccept):
                  retval = True
               elif (ord(response[0]) == self._AccessReject):
                  retval = False
               else:
                  continue

               if retval is not None:
                  break
      else:
         retval = False

      return retval
